#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>
#include <semaphore.h>
#include <errno.h>

#include "other_functions.h"
#include "serial.h"

//#define DEBUG_SHOW_SERIALPORT_DATA

/*  \brief Envía un dato al buffer de transmisión. No lo envía directamente, pero se enviará enseguida
 *  \param p  estructura de datos del interfaz de control del puerto, inicializado con empezar_lectura_serialport
 *  \param Data buffer con los datos a enviar
 *  \param SizeData número de datos a enviar
 *  \return El número de datos puestos en el buffer
 */
long Write_Port_buffer(serial_params_t *p,unsigned char Data[],int SizeData, bool reenvio)
{
    int i=0;
    dato_TX_t dato_tx;

    if (p->pport == INVALID_HANDLE_VALUE)
	return 0;

    //bloque el acceso al buffer y a TX, por exclusión mutua
    pthread_mutex_lock(&p->mutex_buffer_tx);
    pthread_mutex_lock(&p->mutex_buffer_ack);

    for (i=0; i<SizeData; i++)
    {
	//pone los datos enviados en la cola de ACK
	dato_tx.dato = Data[i];
	gettimeofday(&dato_tx.t, NULL);
	p->buffer_ack.push(dato_tx);
    }
    //envía los datos
    SizeData = Write_Port(p->pport, Data, SizeData);
    usleep(1500);
#ifdef DEBUG_SHOW_SERIALPORT_DATA
    printf("written ");
    for (i=0; i<SizeData; i++)
    {
	printf("0x%X ", Data[i]);
    }
    printf(" (%d datos)\n", SizeData);
#endif
    //desbloquea la cola ACK
    pthread_mutex_unlock(&p->mutex_buffer_ack);
    //desbloquea el acceso a TX
    pthread_mutex_unlock(&p->mutex_buffer_tx);

    if (reenvio)
	set_dato_reenviar(p, Data[0], p->secs_reenviar);

    return SizeData;
}

long Write_Port_buffer(serial_params_t *p,unsigned char Data, bool reenviar)
{
    return Write_Port_buffer(p, &Data, 1, reenviar);
}


/* \brief hilo de lectura del puerto serie
 */
void * pthread_func_dato_recibido(void * params)
{
    unsigned char c[10];
    dato_TX_t lastsent;
    struct timeval  t2;
    int n, i;
    serial_params_t *p = (serial_params_t*)params;
    bool seguir = 1;

    printf("Serial Port listening\n");
    while (seguir && !p->quit)
    {
	//lectura de datos recibidos.
	//Lectura bloqueante. Acaba pasado el tiempo del timeout (funcion Set_Time(...))
	//o cuando se ha recibido el número de datos indicado, el menor de Set_Time o el datasize pasado.
	n = read(p->pport, c, 5);
	//si venció el timeout, n==0, no se ha leído nada
	if (n)
	{
	    gettimeofday(&t2, NULL);
	    c[n] = 0;

	    for (i=0; i<n; i++)
	    {
#ifdef DEBUG_SHOW_SERIALPORT_DATA
		printf("read. 0x%X)\n", c[i]);
#endif
		// si no hay datos en la cola, pero se ha recibido un dato:
		// o error fatal, ¡kernel panic!, o el PIC a enviado algo porque iniciativa própia
		
		//bloque la cola de acks
		pthread_mutex_lock(&p->mutex_buffer_ack);

		if (p->buffer_ack.empty())
		{
		    //desbloquea el acceso a la cola
		    pthread_mutex_unlock(&p->mutex_buffer_ack);
		    printf("Error de reccpcón de datos inexperados\n");
		    break;
		}
		else
		{
		    //extrae el último dato enviado de la cola de Ya Enviados
		    lastsent = p->buffer_ack.front();
		    float diff;
		    if ((diff=difftime_ms(t2, lastsent.t)) >= 1000.0)
		    {
			//desbloquea el acceso a la cola
			pthread_mutex_unlock(&p->mutex_buffer_ack);
			//llama a la función de error del puerto
			p->datos_perdidos ++;
			if (p->callback_rx != NULL)
			    seguir = p->callback_rx(-1, lastsent.dato, diff, p, p->params);
			//puesto que no se ha procesado ningún dato del read(p->pport,...) 
			// se decrementa el contador del bucle for
			i--;
		    }
		    else
		    {
			p->buffer_ack.pop();
			//desbloquea el acceso a la cola
			pthread_mutex_unlock(&p->mutex_buffer_ack);
			//llama a la función de comprobación, al callback
			if (p->callback_rx != NULL)
			    seguir = p->callback_rx(c[i], lastsent.dato, diff, p, p->params);
		    }
		}		
	    }
	    
	    //un pequeño retardo para no sobrecargar.
	    //como de todos modos la recepción de datos a 9600 baud es leta, no se nota...
	    usleep(5000);
	}
	else
	{
	    //comprueba datos antiguas del buffer ack
	    //bloque la cola de acks
	    pthread_mutex_lock(&p->mutex_buffer_ack);
	    while (!p->buffer_ack.empty() && seguir)
	    {
		//extrae el último dato enviado de la cola de Ya Enviados
		lastsent = p->buffer_ack.front();
		gettimeofday(&t2, NULL);
		float diff;
		if ((diff=difftime_ms(t2, lastsent.t)) >= 1000.0)
		{
		    //llama a la función de error del puerto
		    p->datos_perdidos ++;
		    if (p->callback_rx != NULL)
			seguir = p->callback_rx(-1, lastsent.dato, diff, p, p->params);
		}
		else
		    break;
	    }			
	    //desbloquea el acceso a la cola
	    pthread_mutex_unlock(&p->mutex_buffer_ack);
	}
    }
    
    printf("Final de escucha del puerto serie\n");
    return NULL;
}


/* \brief Hilo que se encarga de reenviar un dato cada TIME_RETX segundos
 */
void * pthread_func_timer_RETX(void * params)
{
    serial_params_t * p = (serial_params_t*)params;
    
    while (!p->quit)
    {
	//reenvía se el dato a reenviar es distinto de 0
	if (p->dato_reenviar)
	{
	    //pone el dato a reenviar en la cola de TX
	    Write_Port_buffer(p, &p->dato_reenviar, 1, false);
#ifdef DEBUG_SHOW_SERIALPORT_DATA	    
	    printf("reenviado cada %d sec\n", p->secs_reenviar);
#endif
	    sleep(p->secs_reenviar);//TIME_RETX);
	}
	else 
	{
	    //si el dato a reenviar es 0, pone el hilo a dormir
	    sem_wait(&p->sem_retx);
	}
    }

    return NULL;
}


/* \brief Abre el puerto serie e Inicializa el interfaz de comunicación por el puerto serie con la placa del PIC
 * \param p Puntero a la estructrua de datos de la interfaz de control
 * \param portname Nombre del puerto serie a abrir
 * \return true si todo fue bien. false si no se puedo abrir el puerto
 */
bool empezar_lectura_serialport(serial_params_t *p, const char * portname)
{
    //hilos para RX, TX y RE-TX
    int error;
    p->datos_recibidos = 0;
    p->datos_perdidos = 0;

    // Abre el puerto serie
    if ((p->pport=Open_Port(portname, O_RDWR)) == INVALID_HANDLE_VALUE)  
    {
	printf("No se pudo configurar el puerto\n");
	return false;
    }
    
    // guarda la configuracion antigua del puerto 
    if (!Get_Configure_Port(p->pport, &p->OldConf))     
    {
	printf("No se pudo obtener la configuración del puerto\n");
	return false;
    }

    // Configura el puerto serie 
    if (!Configure_Port(p->pport,9600,"8N1", &p->NewConf))   
    {
	printf("No se pudo configurar el puerto\n");
	return false;
    }

    //sin control de flujo
    Set_Hands_Haking(p->pport, 0); 
    
    // configura la lectura con un timeout de 2 segundos,
    // y devuelve de la llamada de lectura tras cada byte recibido.
    Set_Time(p->pport, 15, 0);

    //Inicialización de la estructura serial_params_t con los datos de control del puerto:

    //buffers/colas de TX y ACK, y semáforos de control de las colas
    pthread_mutex_init( &p->mutex_buffer_tx, NULL);
    pthread_mutex_init( &p->mutex_buffer_ack, NULL); 
    //semáforo para bloquear el hilo de tx si no hay datos en la cola de tx
    sem_init(&p->sem_retx, 0, 0);
    //indica que de momento no se reenvía de forma cíclica ningún dato
    p->dato_reenviar = 0;

    p->callback_rx = NULL;
    p->params = NULL;
    p->quit = false;

    //hilo de lectura del puerto serie
    //pthread_dato_recibido es la función del hilo
    //p el puntero de datos que se pasa al hilo
    printf("Thread_Create( Thread_Func_Dato_Recibido ...);\n");
    error = pthread_create(&p->hiloRX, NULL, pthread_func_dato_recibido, p);
    if (error)
    {
	printf("Error al crear el hilo de Lectura\n");
    }
    //hilo de RE-TX
    printf("Thread_Create( Thread_Func_Timer_Retx ...);\n");
    error = pthread_create (&p->hiloRETX, NULL, pthread_func_timer_RETX, p);
    if (error)
    {
	printf("Error al crear el hilo de Reenvio\n");
    }

    return true;
}    


/* \brief Cierra el puerto serie y el interfaz de comunicación por el puerto serie con la placa del PIC
 * \param p Puntero a la estructrua de datos de la interfaz de control
 */
void terminar_lectura_serialport(serial_params_t *p)
{
    //indica que quiere acabar los hilos
    p->quit = true;

    if (p->pport == INVALID_HANDLE_VALUE)
	return;

    Set_Time(p->pport, 0, 0); 
    tcflush(p->pport, TCIOFLUSH);

    //desbloquea el hilo de tx
    sem_post(&p->sem_retx);
    sem_post(&p->sem_retx);

    //acaba con lo hilos
    pthread_cancel(p->hiloRX);
    pthread_cancel(p->hiloRETX);
    pthread_join(p->hiloRX, NULL);
    pthread_join(p->hiloRETX, NULL);


    //Pone la antigua configuracion del puerto 
    Set_Configure_Port(p->pport, p->OldConf);     

    //cerrar puerto serie y colas
    Close_Port(p->pport);   
    printf("Serial port closed.\n");
    p->pport = INVALID_HANDLE_VALUE;
    pthread_mutex_destroy(&p->mutex_buffer_ack);
    pthread_mutex_destroy(&p->mutex_buffer_tx);
    sem_destroy(&p->sem_retx);
}


/* \brief Indica un dato a reenviar por el puerto serie cada TIME_RETX segundos
 * \param p Puntero a la estructrua de datos de la interfaz de control
 * \param key Dato a reenviar cada TIME_RETX segundos. Si key =0, no reenvia nada
 */
void set_dato_reenviar(serial_params_t * p, unsigned char key, int secs)
{
    //si key == 0, no reenviará nada 
    p->dato_reenviar = key;
    p->secs_reenviar = secs;
    if (key)
    {
	int v;
	sem_getvalue(&p->sem_retx, &v);
	if (v == 0)
	    sem_post(&p->sem_retx);
    }
}

bool is_open_port(serial_params_t * p)
{
    if (p->pport ==  INVALID_HANDLE_VALUE)
	return false;
  
    return true;
}


bool GetHayDatosRecibidos(serial_params_t *p)
{
    if (!is_open_port (p))
	return false;
    while (!p->datos_recibidos && !p->datos_perdidos)
	;
    return p->datos_recibidos;
}
